/**
 ** gamelib
 *
 */

define(['oj/three', 'oj/box2d', 'oj/dat'], function(THREE, Box2D, dat) {
    
    window['gamelib'] = {}
    
    
    
    // Constants
    gamelib['CLIENT_FIREFOX']    = /Firefox/i.test(navigator.userAgent)
    gamelib['DISPLAY_DT']        = ~~(1000 / 50)
    gamelib['PHYSICS_DT']        = 1 / 60
    gamelib['GRAVITY']           = new Box2D.Common.Math.b2Vec2(0, 0)
    gamelib['FAR_CLIP']          = 50000
    gamelib['RESOLUTION_WIDTH']  = 800
    gamelib['RESOLUTION_HEIGHT'] = 600
    gamelib['camera']            = null
    gamelib['target']            = null
    gamelib['scene']             = null
    gamelib['renderer']          = null
    gamelib['world']             = null
    gamelib['mouse']             = {
        offset                   : new THREE.Vector2,
        pos3d                    : new THREE.Vector3,
        pos2d                    : new Box2D.Common.Math.b2Vec2(0, 0),
        pointing                 : null,
        dragging                 : null,
        joint                    : null,
        ray                      : new THREE.Ray,
        projector                : new THREE.Projector(),
        intersects               : null,
        rendererOffset           : new THREE.Vector2
    };
    gamelib['roots']             = []
    gamelib['blocks']            = []
    gamelib['meshes']            = []
    
    
    gamelib['noListener'] = function() {}
    
    
    
    gamelib['Block'] = function(/*Object*/properties) {
        /**
         ** Block - A visual and physical entity
         */
        
        var self = {}
        
        
        self['setParent'] = function(/*Block*/parent) {
            /**
             * Sets the parent.
             *
             * If parent is null, this Block becomes a root Block, which defines
             * a body in the physical world. None root Blocks define fixtures
             * attached to the roots body.
             *
             */
            
            // if parent is specified, but the same as current parent then do nothing
            if (parent != null && parent === this.parent)
				return
            
            // is this currently a root Block?
            if (this.body != null) {
                
                // currently a root Block, so detach this and descendant fixtures from body
                this.detachFixtures(this.body)
                
                // destroy body
                gamelib.world.DestroyBody(this.body)
                this.body = null
                
                // remove from list of root blocks
                gamelib.roots.splice(gamelib.roots.indexOf(this), 1)
                
            } else if (this.parent != null) {
                
                // not currently a root Block but is attached, so detach from parents child list
                this.parent.blocks.splice(this.parent.blocks.indexOf(this), 1)
                
                // detach this and descendant fixtures from roots body
                this.detachFixtures(this.root.body)
                
                // adjust position back to world coordinates
                while (this.parent != null) {
                    this.mesh.position.x += this.parent.mesh.position.x
                    this.mesh.position.y += this.parent.mesh.position.y
                    this.mesh.position.z += this.parent.mesh.position.z
                    this.parent = this.parent.parent
                }
            }
            
            // is this going to be a root Block?
            if (parent == null) {
                
                // going to be a root Block, so create own body
                var bodydef = new Box2D.Dynamics.b2BodyDef
                bodydef.type = (this.fixed) ?
                    Box2D.Dynamics.b2Body.b2_staticBody :
                    bodydef.type = Box2D.Dynamics.b2Body.b2_dynamicBody
                bodydef.linearDamping = this.damping
                bodydef.angularDamping = this.damping
                bodydef.position.Set(this.mesh.position.x, this.mesh.position.z)
                this.body = gamelib.world.CreateBody(bodydef)
                (this.active || this.body.SetActive(false))
                
                // add to list of root blocks
                gamelib.roots.push(this)
                
                // attach this mesh to scene (removing it from previous attachment if any)
                gamelib.scene.add(this.mesh)
                
                // set self as root with no parent
                this.setRoot(this)
                
            } else {
                
                // not going to be a root Block, so set root as that of new parents
                this.root = parent
                while (this.root != null && this.root.body == null)
                    this.root = this.root.parent
                this.setRoot(this.root)
                
                // adjust position relative to parent
                this.parent = parent
                while (this.parent != null) {
                    this.mesh.position.x = this.mesh.position.x - this.parent.mesh.position.x
                    this.mesh.position.y = this.mesh.position.y - this.parent.mesh.position.y
                    this.mesh.position.z = this.mesh.position.z - this.parent.mesh.position.z
                    this.parent = this.parent.parent
                }
                
                // attach this mesh to parents mesh
                parent.mesh.add(this.mesh)
                
                // add this block to parents children
                parent.blocks.push(this)
                
            }
            
            // set parent to new parent
            this.parent = parent
            
            // attach this and descendant fixtures to new root body
            if (this.root != null)
                this.attachFixtures(this.root.body)
            
            return this
        }        
        
        
        self['addChild'] = function(/*Block*/block) {
            /**
             * Adds a Block as child.
             */
            
            block.setParent(this)
            
            return this
        }
        
        
        
        self['setRoot'] = function(/*Block*/root) {
            /**
             * Set a Block as root.
             */
            
            this.root = root
            this.setRoot_i = 0
            for (; this.setRoot_i < this.blocks.length; this.setRoot_i++)
                this.blocks[this.setRoot_i].setRoot(root)
        }
        
        
        
        self['detachFixtures'] = function(/*b2Body*/body) {
            /**
             * Removes blocks fixture from body.
             */
            
            if (this.fixture != null)
				body.DestroyFixture(this.fixture)
            this.detachFixtures_i = 0
            for (; this.detachFixtures_i < this.blocks.length; this.detachFixtures_i++)
                this.blocks[this.detachFixtures_i].detachFixtures(body)
        }
        
        
        self['attachFixtures'] = function(/*b2Body*/body) {
            /**
             * Sets the blocks fixture to the root body.
             */
            
            // attach a new fixture for the block to body
            this.fixture = new Box2D.Dynamics.b2FixtureDef
            this.fixture.density = this.density
            this.fixture.friction = this.friction
            this.fixture.restitution = this.restitution
            this.fixture.shape = new Box2D.Collision.Shapes.b2PolygonShape
            this.fixture.shape.SetAsBox(
                (this.width * 0.5),
                (this.depth * 0.5)
            )
            
            // position shape relative to body
            this.fixture.shape.m_centroid.x = this.mesh.position.x - body.m_xf.position.x
            this.fixture.shape.m_centroid.y = this.mesh.position.z - body.m_xf.position.y
            
            this.fixture = body.CreateFixture(this.fixture)
            
            this.attachFixtures_i = 0
            for (; this.attachFixtures_i < this.blocks.length; this.attachFixtures_i++)
                this.blocks[this.attachFixtures_i].attachFixtures(body)
        }
        
        
        
        self['followMouse'] = function(/*boolean*/enable) {
            /**
             * Follow the mouse.
             */
            
            gamelib.mouse.dragging = null
            if (!enable)
				return
            this.setCollisionMask(0xFFFE)
            gamelib.mouse.dragging = this
        }
        
        
        
        self['onMouseOver']  = gamelib.noListener //function()
        
        self['onMouseDown']  = gamelib.noListener //function(/*integer*/button)
        
        self['onMouseWheel'] = gamelib.noListener //function(/*integer*/delta)
        
        self['onMouseDrag']  = gamelib.noListener //function()
        
        
        
        self['setPosition'] = function(/*float*/x, /*float*/y, /*float*/z) {
            /**
             ** Move to specified location.
             */
            
            this.mesh.position.set(x, y, z)

            if (this.body != null) {
                this.body.m_xf.position.x = x
                this.body.m_xf.position.y = z
            }
        }
        
        
        
        self['setCollisionCategory'] = function(/*byte*/category) {
            /**
             * Set the collision category filter (default 0x0001).
             */
            
            var fixtures = this.root.body.GetFixtureList(),
                filter = fixtures.GetFilterData()
            
            filter.categoryBits = category
            fixtures.SetFilterData(filter)
        }
        
        
        
        self['setCollisionMask'] = function(/*byte*/mask) {
            /**
             * Set the collision mask filter (default 0xFFFF).
             */
            
            var fixtures = this.root.body.GetFixtureList(),
                filter = fixtures.GetFilterData()
            
            filter.maskBits = mask
            fixtures.SetFilterData(filter)
        }
        
        
        
        self['setCollisionGroup'] = function(/*integer*/index) {
            /**
             * Set the collision group index filter (default 0).
             */
            
            var fixtures = this.root.body.GetFixtureList(),
                filter = fixtures.GetFilterData()
            
            filter.groupIndex = index
            fixtures.SetFilterData(filter)
        }
        
        
        
        // initialise a new block instance
        self['name'] = (properties && properties.name) || 'BLOCK'
        self['fixed'] = (properties && properties.fixed) || false
        self['width'] = (properties && properties.width) || 1
        self['height'] = (properties && properties.height) || 1
        self['depth'] = (properties && properties.depth) || 1
        self['faces'] = (properties && properties.faces) || []
        self['map'] = (properties && properties.map) || null
        self['colour'] = (properties && properties.colour) || 0xffffff
        self['opacity'] = (properties && properties.opacity) || 1
        self['ambient'] = (properties && properties.ambient) || 0x666666
        self['shininess'] = (properties && properties.shininess) || 100
        self['specular'] = (properties && properties.pecular) || 0x222222
        self['active'] = (properties && properties.active) || false
        self['density'] = (properties && properties.density) || 1
        self['friction'] = (properties && properties.friction) || 0.2
        self['restitution'] = (properties && properties.restitution) || 0.3
        self['damping'] = (properties && properties.damping) || 1
        
        var i = 0,
            faces = []
        
        for (; i < self.faces.length; i++) {
            self.faces[i]['map'] = self.faces[i].map || null,
            self.faces[i]['colour'] = self.faces[i].colour || 0xffffff,
            self.faces[i]['opacity'] = self.faces[i].opacity || 1,
            self.faces[i]['ambient'] = self.faces[i].ambient || 0x666666,
            self.faces[i]['shininess'] = self.faces[i].shininess || 100,
            self.faces[i]['specular'] = self.faces[i].specular || 0x222222,
            faces.push(
                new THREE.MeshLambertMaterial({
                    color: self.faces[i].colour,
                    opacity: self.faces[i].opacity,
                    shading: THREE.SmoothShading,
                    ambient: self.faces[i].ambient,
                    shininess: self.faces[i].shininess,
                    specular: self.faces[i].specular,
                    map: self.faces[i].map
                })
            )
        }
        
        self['blocks'] = []
        
        self['mesh'] = new THREE.Mesh(
            (faces.length > 0) ? new THREE.CubeGeometry(self.width, self.height, self.depth, faces) :
            new THREE.CubeGeometry(self.width, self.height, self.depth),
            new THREE.MeshLambertMaterial({
                color: self.colour,
                opacity: self.opacity,
                shading: THREE.SmoothShading,
                ambient: self.ambient,
                shininess: self.shininess,
                specular: self.specular,
                map: self.map
            })
        )
        
        self.mesh.castShadow = true
        self.mesh.receiveShadow  = true
        
        self.setPosition(
            (properties && properties.x) || 0,
            (properties && properties.y) || 0,
            (properties && properties.z) || 0
        )
        
        gamelib.scene.add(self.mesh)
        gamelib.meshes.push(self.mesh)
        gamelib.blocks.push(self)
        
        return self
    }
    
    
    gamelib['meshToBlock'] = function(/*THREE.Mesh*/mesh) {
        /**
         **Returns the Block of the specified THREE.Mesh.
         */
        
        gamelib.meshToBlockIndex = gamelib.blocks.length
        
        while (gamelib.meshToBlockIndex--) {
            if (gamelib.blocks[gamelib.meshToBlockIndex].mesh === mesh)
                return gamelib.blocks[gamelib.meshToBlockIndex]
        }
        
        return null
    }
    
    
    gamelib['onMouseMove'] = gamelib.noListener //function()
    
    
    gamelib['onMouseDown'] = gamelib.noListener //function(/*integer*/button)
    
    
    gamelib['onMouseWheel'] = gamelib.noListener //function(/*integer*/delta)
    
    
    gamelib['onKeyDown'] = gamelib.noListener //function(/*integer*/key)
    
    
    gamelib['syncsunwithcamera'] = function () {
        gamelib.sun.position.y = gamelib.scene_size*0.5 + gamelib.camera.position.y
        if (gamelib.sun.position.y > gamelib.scene_size)
            gamelib.sun.position.y = gamelib.scene_size
        gamelib.sun.shadowCameraFar = gamelib.scene_size + gamelib.sun.position.y
    }
    
    
    
    gamelib['init'] = function (/*HTMLElement*/container, /*integer*/scene_size) {
        /**
         * Initialise the physics world and rendering context.
         */
        
        gamelib.scene_size = scene_size
        
        gamelib.scene = new THREE.Scene()
        
        // cameras
        gamelib.camera = new THREE.PerspectiveCamera(20, container.offsetWidth / container.offsetHeight, 1, gamelib.FAR_CLIP)
        gamelib.target = new THREE.Vector3(0, 100, 0)
        gamelib.camera.lookAt(gamelib.target)
        
        gamelib.scene.add(gamelib.camera)
        
        //gamelib.renderer = new THREE.CanvasRenderer();
        gamelib.renderer = new THREE.WebGLRenderer()
        gamelib.renderer.setSize(gamelib.RESOLUTION_WIDTH, gamelib.RESOLUTION_HEIGHT)
        gamelib.renderer.shadowMapEnabled = true
        gamelib.renderer.shadowMapSoft = true
        gamelib.renderer.shadowCameraFov = 50
        
        
        gamelib.world = new Box2D.Dynamics.b2World(gamelib.GRAVITY, true)
        
        container.appendChild(gamelib.renderer.domElement)
        
        gamelib.ambientLight = new THREE.AmbientLight(0x222222)
        gamelib.scene.add(gamelib.ambientLight)
        
        gamelib.sun = new THREE.DirectionalLight(0xffffff)
        gamelib.sun.target.position.y = -gamelib.scene_size*0.5
        gamelib.sun.shadowCameraVisible = true
        gamelib.sun.castShadow = true
        gamelib.sun.intensity = 1.1
        gamelib.sun.shadowDarkness = 0.7
        gamelib.sun.shadowBias = 0
        gamelib.sun.shadowCameraNear = 1
        gamelib.sun.shadowMapWidth  = 4096
        gamelib.sun.shadowMapHeight = 4096
        gamelib.sun.shadowCameraLeft = -gamelib.scene_size*0.5
        gamelib.sun.shadowCameraRight = gamelib.scene_size*0.5
        gamelib.sun.shadowCameraTop = gamelib.scene_size*0.5
        gamelib.sun.shadowCameraBottom = -gamelib.scene_size*0.5
        gamelib.sun.target.add(gamelib.sun)
        gamelib.scene.add(gamelib.sun.target)
        
        gamelib.camera.position.set(0, 0, 1000)
        gamelib.fanY(500)
        
        gamelib.syncsunwithcamera()
        
        window.addEventListener('resize', function(/*Event*/event) {
            gamelib.camera.aspect = container.offsetWidth / container.offsetHeight
            gamelib.camera.updateProjectionMatrix()
        }, false)
        
        document.addEventListener('keydown', function(/*Event*/event) {
            event.preventDefault()
            gamelib.onKeyDown(event.which)
        }, false)
        
        document.addEventListener(gamelib.CLIENT_FIREFOX ? "DOMMouseScroll" : "mousewheel", function(/*Event*/event) {
            event.preventDefault()
            gamelib.mousewheel_delta = gamelib.CLIENT_FIREFOX ? -event.detail / 3 : event.wheelDelta / 120
            if (gamelib.mouse.dragging != null)
                gamelib.mouse.dragging.onMouseWheel(gamelib.mousewheel_delta)
            gamelib.onMouseWheel(gamelib.mousewheel_delta)
        }, false)
        
        document.addEventListener('mousedown', function(/*Event*/event) {
            event.preventDefault()
            if (gamelib.mouse.dragging != null)
                gamelib.mouse.dragging.onMouseDown(event.which)
			else if (gamelib.mouse.pointing != null)
                gamelib.mouse.pointing.onMouseDown(event.which)
            gamelib.onMouseDown(event.which)
        }, false)
        
        document.addEventListener('mousemove', function(/*Event*/event) {
            event.preventDefault()
            
            gamelib.mouse.offset.x = (((document.documentElement.scrollLeft || document.body.scrollLeft) + event.clientX - gamelib.mouse.rendererOffset.x) / container.offsetWidth) * 2 - 1
            gamelib.mouse.offset.y = (((document.documentElement.scrollTop || document.body.scrollTop) + event.clientY - gamelib.mouse.rendererOffset.y) / container.offsetHeight) * 2 - 1
            
            gamelib.mouse.pos3d.x = gamelib.mouse.offset.x
            gamelib.mouse.pos3d.y = -gamelib.mouse.offset.y
            gamelib.mouse.pos3d.z = 0.5
            
            gamelib.mouse.projector.unprojectVector(gamelib.mouse.pos3d, gamelib.camera)
            
            gamelib.mouse.ray.origin.x = gamelib.camera.position.x
            gamelib.mouse.ray.origin.y = gamelib.camera.position.y
            gamelib.mouse.ray.origin.z = gamelib.camera.position.z
            
            gamelib.mouse.ray.direction = gamelib.mouse.pos3d.subSelf(gamelib.camera.position).normalize()
            gamelib.mouse.intersects = gamelib.mouse.ray.intersectObjects(gamelib.meshes)
            
            if (gamelib.mouse.dragging != null) {

                gamelib.mousemove_dx = gamelib.camera.position.x - gamelib.mouse.dragging.mesh.position.x,
                gamelib.mousemove_dy = gamelib.camera.position.y - gamelib.mouse.dragging.mesh.position.y,
                gamelib.mousemove_dz = gamelib.camera.position.z - gamelib.mouse.dragging.mesh.position.z
                gamelib.mouse.distance = Math.sqrt(
                    (gamelib.mousemove_dx * gamelib.mousemove_dx) +
                    (gamelib.mousemove_dy * gamelib.mousemove_dy) +
                    (gamelib.mousemove_dz * gamelib.mousemove_dz)
                )
                gamelib.mouse.pos2d.x = gamelib.camera.position.x + gamelib.mouse.distance * gamelib.mouse.ray.direction.x
                gamelib.mouse.pos2d.y = gamelib.camera.position.z + gamelib.mouse.distance * gamelib.mouse.ray.direction.z
                
                
                // is dragging currently attached to a parent?
                if (gamelib.mouse.dragging.parent != null)
                    
                    // adjust position relative to parent
                    gamelib.mouse.dragging.setPosition(
                        gamelib.mouse.pos2d.x - gamelib.mouse.dragging.parent.mesh.position.x,
                        gamelib.mouse.dragging.parent.mesh.position.y,
                        gamelib.mouse.pos2d.y - gamelib.mouse.dragging.parent.mesh.position.z
                    )
                    
                else
                    
                    // adjust position relative to world
                    gamelib.mouse.dragging.setPosition(
                        gamelib.mouse.pos2d.x,
                        gamelib.mouse.dragging.mesh.position.y,
                        gamelib.mouse.pos2d.y
                    );
		        
                gamelib.mouse.dragging.onMouseDrag()

            } else {

                if (gamelib.mouse.intersects.length > 0) {
                    gamelib.mouse.pointing = gamelib.meshToBlock(gamelib.mouse.intersects[0].object)
                    gamelib.mouse.pointing.onMouseOver()
                } else
                    gamelib.mouse.pointing = null

            }
            
            gamelib.onMouseMove()

        }, false)
        
        return gamelib

    }
    
    
    gamelib['play'] = function () {
        (function run(time){
            window.requestAnimationFrame(run)
            
            gamelib.runIndex = gamelib.roots.length
            while (gamelib.runBlock--) {
                gamelib.runBlock = gamelib.roots[gamelib.runIndex]
                gamelib.runBlock.mesh.position.x = gamelib.runBlock.body.m_xf.position.x
                gamelib.runBlock.mesh.position.z = gamelib.runBlock.body.m_xf.position.y
                gamelib.runBlock.mesh.rotation.y = gamelib.runBlock.body.GetAngle()
            }
            
            gamelib.renderer.render(gamelib.scene, gamelib.camera)
            
            gamelib.world.Step(gamelib.PHYSICS_DT, 10, 10)
            gamelib.world.ClearForces()
        })()
        
    }
    
    
    gamelib['shiftX'] = function (delta) {
        /**
         * Shift the camera and target along the X axis.
         */
        
        gamelib.camera.position.x += delta
        gamelib.target.x += delta
        gamelib.sun.target.position.x = gamelib.target.x
        gamelib.camera.lookAt(gamelib.target)
    }
    
    
    gamelib['shiftY'] = function (delta) {
        /**
         * Shift the camera and target along the Y axis.
         */
        
        gamelib.camera.position.y += delta
        gamelib.target.y += delta
        gamelib.sun.target.position.y = gamelib.target.y
        gamelib.camera.lookAt(gamelib.target)
    }
    
    
    gamelib['fanY'] = function (delta) {
        /**
         * Fan to camera along the Y axis.
         *
         * Fanning moves the position of the camera on the axis without moving its target.
         */
        
        
        gamelib.camera.position.y += delta
        gamelib.camera.lookAt(gamelib.target)
        gamelib.syncsunwithcamera()
    };
    
    
    gamelib['shiftZ'] = function (delta) {
        /**
         * Shift the camera and target along the Z axis.
         */
        
        gamelib.camera.position.z += delta
        gamelib.target.z += delta
        gamelib.sun.target.position.z = gamelib.target.z
        gamelib.camera.lookAt(gamelib.target)
    }
    
    
    gamelib['loadTextures'] = function(/*Array*/urls, /*Function*/callback) {
        var images = {},
            textures = {}

        urls.forEach(function(url, i) {

			if (images.hasOwnProperty(url))
                return

            images[url] = new Image

            images[url].onload = function() {
                var texture = new THREE.Texture(images[url])
                texture.needsUpdate = true
                images[url] = null
                textures[url] = texture
                if (Object.keys(textures).length === urls.length)
                    callback(textures)
            }
            images[url].src = url
        })
    }
    
    return gamelib
    
})
